// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SnapObjects.Commons;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal class EmbeddedLoader
    {
        private readonly DataContext _context;
        private readonly ModelSelectBuilder _modelSelectBuilder;

        public EmbeddedLoader(
            DataContext context,
            ModelSelectBuilder modelSelectBuilder)
        {
            _context = context;
            _modelSelectBuilder = modelSelectBuilder;
        }

        public IReadOnlyList<IAdoDbParameter> BoundParameters { get; private set; }

        public bool IsResultModel { get; private set; }

        public bool IsResultCollection { get; private set; }

        public object LoadEmbedded(
            object model,
            string property,
            IReadOnlyList<IAdoDbParameter> parentParameterValues) //paramaters passed in to load parent
        {
            Check.NotNull(model, nameof(model));
            Check.NotNull(property, nameof(property));

            var modelMeta = ModelMetaManager.GetModelMeta(model.GetType());
            var accessor = ModelFactoryFactory.Create(model.GetType()).GetAccessor();

            if (!modelMeta.PropertyMap.TryGetValue(property, out var propertyMeta) ||
                    !propertyMeta.IsEmbedded)
            {
                throw new InvalidOperationException("Property is not exists or not a embedded property, property: " + property);
            }

            var embedded = propertyMeta.SqlEmbedded != null ?
                        (IEmbedded)propertyMeta.SqlEmbedded : propertyMeta.ModelEmbedded;

            object result = null;
            var sqlExecutor = new ModelSqlExecutor(_context);

            var modelType = this.GetModelType(embedded, propertyMeta.EmbeddedType);
            var isModel = propertyMeta.EmbeddedType.IsModel();

            var definedParameters = this.GetSqlAndDefinedParameters(
                                            propertyMeta,
                                            out var sqlText);

            var adoDbParameters = this.BindParameterValues(
                                            model,
                                            embedded.ParamValue,
                                            definedParameters,
                                            parentParameterValues);

            if (propertyMeta.IsContainerType)
            {
                result = isModel ?
                        sqlExecutor.ExecuteSqlQuery(modelType, sqlText, adoDbParameters) :
                            sqlExecutor.ExecuteSqlQuery2(propertyMeta.EmbeddedType, sqlText, adoDbParameters);

                if (propertyMeta.DataType.IsArray)
                {
                    result = this.ToArray(propertyMeta.EmbeddedType, result);
                }
            }
            else
            {
                result = isModel ?
                        sqlExecutor.SelectOne(modelType, sqlText, adoDbParameters) :
                            sqlExecutor.Scalar<object>(sqlText, adoDbParameters);
            }

            this.BoundParameters = embedded is IModelEmbedded && !propertyMeta.ModelEmbedded.IsQueryByKey ?
                                        (IReadOnlyList<IAdoDbParameter>)adoDbParameters :
                                            new List<IAdoDbParameter>();

            accessor.SetValue(model, propertyMeta.Name, result);

            this.IsResultModel = isModel;
            this.IsResultCollection = propertyMeta.IsContainerType;

            return result;
        }

        public async Task<object> LoadEmbeddedAsync(
            object model,
            string property,
            IReadOnlyList<IAdoDbParameter> parentParameterValues,
            CancellationToken cancellationToken) //paramaters passed in to load parent
        {
            Check.NotNull(model, nameof(model));
            Check.NotNull(property, nameof(property));

            var modelMeta = ModelMetaManager.GetModelMeta(model.GetType());
            var accessor = ModelFactoryFactory.Create(model.GetType()).GetAccessor();

            if (!modelMeta.PropertyMap.TryGetValue(property, out var propertyMeta) ||
                    !propertyMeta.IsEmbedded)
            {
                throw new InvalidOperationException("Property is not exists or not a embedded property, property: " + property);
            }

            var embedded = propertyMeta.SqlEmbedded != null ?
                        (IEmbedded)propertyMeta.SqlEmbedded : propertyMeta.ModelEmbedded;

            object result = null;
            var sqlExecutor = new ModelSqlExecutor(_context);

            var modelType = this.GetModelType(embedded, propertyMeta.EmbeddedType);
            var isModel = propertyMeta.EmbeddedType.IsModel();

            var definedParameters = this.GetSqlAndDefinedParameters(
                                            propertyMeta,
                                            out var sqlText);

            var adoDbParameters = this.BindParameterValues(
                                            model,
                                            embedded.ParamValue,
                                            definedParameters,
                                            parentParameterValues);

            if (propertyMeta.IsContainerType)
            {
                result = isModel ?
                    await sqlExecutor.ExecuteSqlQueryAsync(modelType, sqlText, adoDbParameters, default, cancellationToken) :
                    await sqlExecutor.ExecuteSqlQuery2Async(propertyMeta.EmbeddedType, sqlText, adoDbParameters, default, cancellationToken);
                
                if (propertyMeta.DataType.IsArray)
                {
                    result = this.ToArray(propertyMeta.EmbeddedType, result);
                }
            }
            else
            {
                result = isModel ?
                    await sqlExecutor.SelectOneAsync(modelType, sqlText, adoDbParameters, cancellationToken) :
                    await sqlExecutor.ScalarAsync<object>(sqlText, adoDbParameters, cancellationToken);
            }

            this.BoundParameters = embedded is IModelEmbedded && !propertyMeta.ModelEmbedded.IsQueryByKey ?
                                        (IReadOnlyList<IAdoDbParameter>)adoDbParameters :
                                            new List<IAdoDbParameter>();

            accessor.SetValue(model, propertyMeta.Name, result);

            this.IsResultModel = isModel;
            this.IsResultCollection = propertyMeta.IsContainerType;

            return result;
        }

        //Get defined parameters of embedded model
        public List<IAdoDbParameter> GetSqlAndDefinedParameters(
            IPocoProperty propertyMeta,
            out string sqlText)
        {
            var definedParameters = new List<IAdoDbParameter>();

            var embedded = propertyMeta.SqlEmbedded != null ?
                (IEmbedded)propertyMeta.SqlEmbedded : propertyMeta.ModelEmbedded;

            if (embedded is ISqlEmbedded tempEmbedded)
            {
                var parser = new SqlSyntaxParser(
                    _context.AdoDbSqlProvider.SqlGenerationHelper,
                    _context.AdoDbSqlProvider.TypeMapper);

                parser.Parse(tempEmbedded.RawSql);

                definedParameters = parser.SqlParameters.Values.ToList();
                sqlText = parser.SqlStatement; //update sql parameter
            }
            else //ModelEmbedded
            {
                var modelEmbedded = propertyMeta.ModelEmbedded;

                sqlText = _modelSelectBuilder.GetSqlText(
                                                modelEmbedded.ModelType,
                                                modelEmbedded.QuerySelector,
                                                modelEmbedded.QueryExpression,
                                                out var sqlParameters);

                if (modelEmbedded.IsQueryByKey)
                {
                    definedParameters = (List<IAdoDbParameter>)ModelParameterHelper.ConvertParameters(
                                            _context,
                                            sqlParameters);
                }
                else
                {
                    definedParameters = (List<IAdoDbParameter>)ModelParameterHelper.ConvertParameters(
                                            _context,
                                            sqlParameters);
                }
            }

            return definedParameters;
        }

        public AdoDbParameter[] BindParameterValues(
            object model,
            string paramValue,
            IReadOnlyList<IAdoDbParameter> definedParameters,
            IReadOnlyList<IAdoDbParameter> parentParameters) //paramaters passed in to load parent
        {
            if (paramValue == null)
            {
                return new AdoDbParameter[0];
            }

            //"$Id, $Name, :ParamName"
            var paramValues = paramValue.Split(',');
            var adoDbParameters = new AdoDbParameter[paramValues.Length];

            if (definedParameters.Count > paramValues.Length)
            {
                throw new InvalidOperationException("Missing parameter value(s), defined parameter count: " +
                        definedParameters.Count + ", parameter value count: " + paramValues.Length + ".");
            }

            var modelMeta = ModelMetaManager.GetModelMeta(model.GetType());
            var accessor = ModelFactoryFactory.Create(model.GetType()).GetAccessor();

            for (var i = 0; i < paramValues.Length; i++)
            {
                //string prefix = "_" + i + "_";
                paramValues[i] = paramValues[i].Trim();

                if (i < paramValues.Length)
                {
                    if (paramValues[i].StartsWith("$"))
                    {
                        var property = paramValues[i][1..];

                        if (modelMeta.PropertyMap.TryGetValue(
                                property, out var propertyMeta))
                        {

                            adoDbParameters[i] = new AdoDbParameter(
                                                        definedParameters[i].ParameterName,
                                                        accessor.GetValue(model, propertyMeta.Name),
                                                        propertyMeta.DataType);
                        }
                        else
                        {
                            throw new InvalidOperationException("Property is not exist, property: " + propertyMeta.Name);
                        }
                    }
                    else if (paramValues[i].StartsWith(":"))
                    {
                        var paramName = paramValues[i][1..];
                        paramName = _context.AdoDbSqlProvider.SqlGenerationHelper
                                        .GenerateParameterName(paramName);

                        var adbDbParam = parentParameters.Where(
                                            o => o.ParameterName.Equals(paramName, StringComparison.OrdinalIgnoreCase))
                                                                .FirstOrDefault();
                        if (adbDbParam != null)
                        {
                            adoDbParameters[i] = new AdoDbParameter(
                                                        definedParameters[i].ParameterName,
                                                        adbDbParam.Value,
                                                        adbDbParam.ClrType);
                        }
                        else
                        {
                            throw new InvalidOperationException("Invalid parameter name,  parameter name: " + paramName);
                        }
                    }
                    else
                    {
                        throw new InvalidOperationException("Unknown parameter,  parameter: " + paramValues[i]);
                    }
                }
                else
                {
                    throw new InvalidOperationException("Unknown parameter,  parameter: " + paramValues[i]);
                }
            }

            return adoDbParameters;
        }

        private object ToArray(Type EmbeddedType, object list)
        {
            var listData = (IList)list;
            var array = Array.CreateInstance(EmbeddedType, listData.Count);

            for (var i = 0; i < listData.Count; i++)
            {
                array.SetValue(listData[i], i);
            }

            return array;
        }

        private Type GetModelType(IEmbedded embedded, Type defaultType)
        {
            if (embedded is ISqlEmbedded)
            {
                return defaultType;
            }
            else
            {
                var modelEmbedded = (IModelEmbedded)embedded;

                return modelEmbedded.ModelType ?? defaultType;
            }
        }
    }
}
